﻿using System;
using System.Configuration;
using System.Reflection;
using NHibernate;
using System.Runtime.Remoting.Messaging;
using System.Web;
using log4net;

namespace NCommet.Modules.Dao
{
    /// <summary>
    /// For asp.net projects that use <see cref="NCommet.Modules.Dao.Storage"/> as the implementation for the <see cref="NCommet.Core.Agents.IPersister"/> interface of NCommet, <see cref="NCommet.Modules.Dao.StorageCleanupModule"/> should be added to the web project's HTTP modules. For each request, it ensures proper termination of <see cref="NCommet.Modules.Dao.NHibernateSessionManager"/>'s transaction and session.
    /// </summary>
    public class StorageCleanupModule : IHttpModule
    {
        private readonly static ILog log = LogManager.GetLogger(typeof(StorageCleanupModule));

        #region IHttpModule Members

        public void Dispose()
        {
        }

        public void Init(HttpApplication context)
        {
            log.Debug("Init");
            context.EndRequest += new EventHandler(context_EndRequest);
        }

        void context_EndRequest(object sender, EventArgs e)
        {
            log.DebugFormat("EndRequest {0}", HttpContext.Current.Request.Url);
            try
            {
                if (NHibernateSessionManager.Instance.InTransaction())
                {
                    log.WarnFormat("Transaction uncommited from {0}", HttpContext.Current.Request.Url);
                    NHibernateSessionManager.Instance.CommitTransaction();
                }
            }
            finally
            {
                if (NHibernateSessionManager.Instance.IsSessionOpen())
                {
                    log.DebugFormat("Closing open session for {0}", HttpContext.Current.Request.Url);
                    NHibernateSessionManager.Instance.CloseSession();
                }
            }
        }

        #endregion
    }

    /// <summary>
    /// Singleton class that manages NHibernate's sessions and transactions. It can create only one session and one transaction for each HTTP request (for a web application) or for each execution code path (for a desktop application). Also, it automatically configures NHibernate and loads the assemblies containing, in embedded resources, the required mapping files.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///     When the singleton class is firstly instantiated, it configures NHibernate, creates the respective <see cref="NHibernate.ISessionFactory"/> and sets <see cref="NCommet.Modules.Dao.CompositeInterceptor"/> as NHibernate's sole <see cref="NHibernate.IInterceptor"/>.
    ///     </para>
    ///     <para>
    ///     The application's configuration file must contain the necessary configuration for NHibernate and also contain some custom application settings that denote which assemblies contain, in embedded resources, the required mapping files to be loaded into NHibernate. An example of such application settings is:
    ///     </para>
    ///     <para>
    ///     <code>
    ///         &lt;appSettings&gt;
    ///             ...
    ///             &lt;add key="HBM_ASSEMBLY_COUNT" value="2"/&gt;
    ///             &lt;add key="HBM_ASSEMBLY_1" value="NCommet.Modules.Dao"/&gt;
    ///             &lt;add key="HBM_ASSEMBLY_2" value="[Fully qualified name of the application's assembly]"/&gt;
    ///         &lt;/appSettings&gt;
    ///     </code>
    ///     </para>
    ///     <para>
    ///     NCommet.Modules.Dao should always be the first in the list, as it contains the mapping files for the basic classes of <see cref="NCommet.Core"/>. The application's assembly should contain, as embedded resources, mapping files for each content class it defines. Each content class must be configured as a &lt;subclass&gt; of <see cref="NCommet.Core.ContentBase"/> in the mapping file.
    ///     </para>
    ///     <para>
    ///     <see cref="NCommet.Modules.Dao.NHibernateSessionManager"/> supports only one current session and transaction per thread. This is achieved by storing the session or transaction in the <see cref="System.Web.HttpContext"/> (if the thread is of a web application) or in the <see cref="System.Runtime.Remoting.Messaging.CallContext"/>. For this reason, use of <see cref="NCommet.Modules.Dao.StorageCleanupModule"/> is recommended for web applications.
    ///     </para>
    /// </remarks>
    public sealed class NHibernateSessionManager
    {
        #region Private fields

        private ISessionFactory sessionFactory;
        private CompositeInterceptor interceptor;
        private NHibernate.Cfg.Configuration cfg;

        #endregion

        #region Thread-safe, lazy Singleton

        public static NHibernateSessionManager Instance
        {
            get
            {
                return Nested.instance;
            }
        }

        private NHibernateSessionManager()
        {
            interceptor = new CompositeInterceptor();

            cfg = new NHibernate.Cfg.Configuration();
            cfg.SetInterceptor(interceptor);

            int hbmAssemblyCount = 0;
            if (string.IsNullOrEmpty(ConfigurationManager.AppSettings["HBM_ASSEMBLY_COUNT"]))
            {
                // the old way, one assembly

                // The following makes sure the the web.config contains a declaration for the HBM_ASSEMBLY appSetting
                if (string.IsNullOrEmpty(ConfigurationManager.AppSettings["HBM_ASSEMBLY"]))
                {
                    throw new ConfigurationErrorsException("NHibernateManager.InitSessionFactory: \"HBM_ASSEMBLY\" must be " +
                        "provided as an appSetting within your config file. \"HBM_ASSEMBLY\" informs NHibernate which assembly " +
                        "contains the HBM files. It is assumed that the HBM files are embedded resources. An example config " +
                        "declaration is <add key=\"HBM_ASSEMBLY\" value=\"MyProject.Core\" />");
                }

                cfg.AddAssembly(Assembly.Load(ConfigurationManager.AppSettings["HBM_ASSEMBLY"]));
            }
            else
            {
                // the new way, many assemblies
                hbmAssemblyCount = Convert.ToInt32(ConfigurationManager.AppSettings["HBM_ASSEMBLY_COUNT"]);
                for (int i = 1; i <= hbmAssemblyCount; i++)
                {
                    string strKey = "HBM_ASSEMBLY_" + i;
                    if (string.IsNullOrEmpty(ConfigurationManager.AppSettings[strKey]))
                        throw new ConfigurationErrorsException("Config mismatch: key #" + i + " was empty");
                    cfg.AddAssembly(Assembly.Load(ConfigurationManager.AppSettings[strKey]));
                }
            }

            sessionFactory = Config.BuildSessionFactory();
        }

        class Nested
        {
            static Nested() { }
            internal static readonly NHibernateSessionManager instance = new NHibernateSessionManager();
        }

        #endregion

        public NHibernate.Cfg.Configuration Config { get { return cfg; } }

        public void AddInterceptor(IInterceptor interceptor)
        {
            this.interceptor.AddInterceptor(interceptor);
        }

        public void RemoveInterceptor(IInterceptor interceptor)
        {
            this.interceptor.RemoveInterceptor(interceptor);
        }

        public ISession GetSession()
        {
            ISession session = threadSession;

            if (session == null)
            {
                session = sessionFactory.OpenSession();
                threadSession = session;
            }

            return session;
        }

        public bool IsSessionOpen()
        {
            ISession session = threadSession;
            return session != null && session.IsOpen;
        }

        public void CloseSession()
        {
            ISession session = threadSession;
            threadSession = null;
            if (session != null && session.IsOpen)
                session.Close();
        }

        public bool InTransaction() { return threadTransaction != null; }

        public void BeginTransaction()
        {
            ITransaction transaction = threadTransaction;

            if (transaction == null)
            {
                transaction = GetSession().BeginTransaction();
                threadTransaction = transaction;
            }
        }

        public void CommitTransaction()
        {
            ITransaction transaction = threadTransaction;

            try
            {
                if (transaction != null && !transaction.WasCommitted && !transaction.WasRolledBack)
                {
                    transaction.Commit();
                    threadTransaction = null;
                }
            }
            catch (HibernateException ex)
            {
                RollbackTransaction();
                throw ex;
            }
        }

        public void RollbackTransaction()
        {
            ITransaction transaction = threadTransaction;

            try
            {
                threadTransaction = null;

                if (transaction != null && !transaction.WasCommitted && !transaction.WasRolledBack)
                {
                    transaction.Rollback();
                }
            }
            catch (HibernateException ex)
            {
                throw ex;
            }
            finally
            {
                CloseSession();
            }
        }

        private T GetContextItem<T>(string key) where T : class
        {
            return (T)(IsInWebContext() ?
                HttpContext.Current.Items[key] :
                CallContext.GetData(key));
        }

        private void SetContextItem<T>(string key, T value) where T : class
        {
            if (IsInWebContext())
                HttpContext.Current.Items[key] = value;
            else
                CallContext.SetData(key, value);
        }

        private ITransaction threadTransaction
        {
            get { return GetContextItem<ITransaction>(TRANSACTION_KEY); }
            set { SetContextItem<ITransaction>(TRANSACTION_KEY, value); }
        }

        private ISession threadSession
        {
            get { return GetContextItem<ISession>(SESSION_KEY); }
            set { SetContextItem<ISession>(SESSION_KEY, value); }
        }

        private static bool IsInWebContext()
        {
            return HttpContext.Current != null;
        }

        private const string SESSION_KEY = "CONTEXT_SESSION";
        private const string TRANSACTION_KEY = "CONTEXT_TRANSACTION";
    }
}
